OptionsBasedMetricRecordingEnricher Class
Namespace: Diginsight.Diagnostics
Assembly: Diginsight.Diagnostics.dll
Configuration-based enricher that adds contextual tags to metrics by extracting specified tag names from activity hierarchies.
public class OptionsBasedMetricRecordingEnricher : IMetricRecordingEnricherInheritance
Object ? OptionsBasedMetricRecordingEnricher
Implements
IMetricRecordingEnricher
Summary
The OptionsBasedMetricRecordingEnricher class automatically extracts business-relevant tags from activities and their parent hierarchy to enrich metrics with contextual dimensions. It uses a declarative, configuration-driven approach where you specify tag names in appsettings.json, and the enricher searches the activity tree to find matching tag values.
Key capabilities: - ? Configuration-driven enrichment - specify tag names in appsettings.json - ? Hierarchical tag search - searches activity and all parent activities - ? Instrument-specific tags - different tags per metric instrument - ? Tag deduplication - combined instrument-specific and general tags with duplicates removed - ? Null-safe extraction - only includes tags with non-null values - ? Virtualizable method - easily extensible for custom enrichment logic - ? Low cardinality support - promotes using categorical tags from configuration
Constructors
OptionsBasedMetricRecordingEnricher(IOptionsMonitor)
Initializes a new instance of the OptionsBasedMetricRecordingEnricher class.
public OptionsBasedMetricRecordingEnricher(
IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions> enricherMonitor
)Parameters
enricherMonitor : IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions>
Monitor for accessing enricher configuration options. Uses IOptionsMonitor to support dynamic configuration changes at runtime without restarting the application.
Remarks
The constructor stores the options monitor which allows: - Access to named options for instrument-specific tag configurations - Hot reload support - configuration changes apply immediately - Default configuration via CurrentValue property
Methods
Configuration
OptionsBasedMetricRecordingEnricherOptions
Configure the enricher in appsettings.json:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"<tag_name_1>",
"<tag_name_2>"
]
}
}Properties
MetricTags : ICollection<string>
List of tag names to extract from activities and include in metrics.
Guidelines: - ? Use low-cardinality tags (status, tier, region, category) - ? Use business-relevant dimensions for filtering and grouping - ? Avoid high-cardinality identifiers (user IDs, order IDs, request IDs) - ? Avoid unbounded values (URLs, timestamps, freeform text)
Named Configuration (Instrument-Specific)
Configure different tags for specific metric instruments:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"environment",
"region",
"customer_tier"
]
},
"diginsight.span_duration": {
"MetricTags": [
"operation_category",
"criticality_level"
]
},
"http.server.request.duration": {
"MetricTags": [
"endpoint_category",
"api_version"
]
}
}How it works: - Section name matches instrument.Name parameter - Instrument-specific tags are combined with general tags - Duplicate tag names are automatically removed - All tags are searched in the activity hierarchy
Resulting tag names:
// For "diginsight.span_duration" instrument:
// ["operation_category", "criticality_level", "environment", "region", "customer_tier"]
// (instrument-specific + general, deduplicated)
// For "http.server.request.duration" instrument:
// ["endpoint_category", "api_version", "environment", "region", "customer_tier"]
// For other instruments:
// ["environment", "region", "customer_tier"]
// (only general tags)Usage Examples
Basic Registration
Register the enricher during application startup:
var builder = WebApplication.CreateBuilder(args);
// Register enricher
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
// Register metric recorder (uses the enricher)
builder.Services.AddSpanDurationMetricRecorder();
var app = builder.Build();
app.Run();Simple Configuration
appsettings.json:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"environment",
"service_version",
"deployment_slot"
]
}
}Application code:
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
environment = "production",
service_version = "2.5.1",
deployment_slot = "blue",
order_id = "12345" // Not in config, won't be extracted
});
// Resulting metric:
// diginsight.span_duration{
// span_name="ProcessOrder",
// status="Ok",
// environment="production",
// service_version="2.5.1",
// deployment_slot="blue"
// } = 250msHierarchical Tag Resolution
Configuration:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"customer_tier",
"operation_type",
"correlation_id"
]
}
}Activity hierarchy:
// Root activity (e.g., from HTTP request middleware)
using var httpActivity = activitySource.StartRichActivity("HttpRequest", new
{
correlation_id = Guid.NewGuid().ToString(),
customer_tier = "enterprise"
});
// Business logic activity (overrides operation_type)
using var businessActivity = activitySource.StartRichActivity("ProcessOrder", new
{
operation_type = "write"
});
// Database activity (inherits from ancestors)
using var dbActivity = activitySource.StartRichActivity("SaveOrder", new
{
// No tags set
});
// Tags extracted for dbActivity:
// correlation_id ? searches dbActivity (null), businessActivity (null), httpActivity ("guid") ?
// customer_tier ? searches dbActivity (null), businessActivity (null), httpActivity ("enterprise") ?
// operation_type ? searches dbActivity (null), businessActivity ("write") ?
// Result: All three tags found in ancestor hierarchyCardinality Control
Use enricher to promote low-cardinality tagging:
? Bad - High cardinality tags in activity:
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
order_id = "order-12345", // Millions of unique values
customer_id = "cust-67890", // Millions of unique values
request_id = Guid.NewGuid() // Unlimited unique values
});
// These high-cardinality values are available for logging/tracing
// but won't become metric tags (not in config)? Good - Low cardinality tags:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"customer_tier",
"order_size_bucket",
"payment_method"
]
}
}using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
// High cardinality - available for logs/traces, not metrics
order_id = "order-12345",
customer_id = "cust-67890",
// Low cardinality - will become metric tags
customer_tier = "premium", // 3 values: free, standard, premium
order_size_bucket = "large", // 4 values: small, medium, large, xlarge
payment_method = "credit_card" // 5 values: credit_card, paypal, invoice, etc.
});
// Resulting metric:
// diginsight.span_duration{
// customer_tier="premium",
// order_size_bucket="large",
// payment_method="credit_card"
// } = 250ms
// Only ~60 unique combinations (3 � 4 � 5) - excellent cardinality!Multiple Enrichers
Combine with custom enrichers for advanced scenarios:
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, DeploymentContextEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, PerformanceTierEnricher>();Custom enricher example:
public class DeploymentContextEnricher : IMetricRecordingEnricher
{
public Tags ExtractTags(Activity activity, Instrument instrument)
{
return
[
new Tag("host", Environment.MachineName),
new Tag("k8s_pod", Environment.GetEnvironmentVariable("POD_NAME")),
new Tag("deployment_version", GetAssemblyVersion())
];
}
}Hot Reload Configuration
Changes to appsettings.json apply immediately without restart:
Initial configuration:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": ["environment", "region"]
}
}After hot reload (add more tags):
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"environment",
"region",
"customer_tier",
"feature_flags"
]
}
}Result: - New activities immediately include the additional tags - No application restart required - Existing in-flight activities use old configuration
Custom Derived Enricher
Extend for custom logic:
public class SmartMetricEnricher : OptionsBasedMetricRecordingEnricher
{
private readonly IFeatureFlagService _featureFlags;
public SmartMetricEnricher(
IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions> enricherMonitor,
IFeatureFlagService featureFlags)
: base(enricherMonitor)
{
_featureFlags = featureFlags;
}
public override Tags ExtractTags(Activity activity, Instrument instrument)
{
// Start with configuration-based tags
var baseTags = base.ExtractTags(activity, instrument).ToList();
// Add computed tags
baseTags.Add(new Tag("ab_test_variant", _featureFlags.GetVariant("checkout_flow")));
// Add conditional tags
if (activity.Duration > TimeSpan.FromSeconds(1))
{
baseTags.Add(new Tag("slow_operation", "true"));
}
return baseTags;
}
}Registration:
builder.Services.AddSingleton<IMetricRecordingEnricher, SmartMetricEnricher>();Tag Extraction Details
Activity Tag Lookup
Tags are extracted using Activity.GetTagItem(string key):
// Activity tags set during creation
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
customer_tier = "premium", // Becomes tag: customer_tier = "premium"
order_value = 1299.99, // Becomes tag: order_value = 1299.99
is_express = true // Becomes tag: is_express = true
});
// Can also be set later
activity.SetTag("retry_count", 0);
activity.SetTag("cache_hit", true);Hierarchical Search Process
Example hierarchy:
HttpRequest (root)
?? customer_id = "cust-123"
?? region = "us-east"
?? ProcessOrder
?? operation_type = "write"
?? SaveToDatabase
?? database = "orders"
?? (current activity)
Tag extraction for “customer_id”:
activity.GetAncestors(includeSelf: true)
// Returns: [SaveToDatabase, ProcessOrder, HttpRequest]
.Select(a => a.GetTagItem("customer_id"))
// Returns: [null, null, "cust-123"]
.FirstOrDefault(v => v != null)
// Returns: "cust-123"Search is efficient: - Stops at first non-null value (early exit) - No unnecessary hierarchy traversal - Lazy evaluation with LINQ
Value Type Support
Tags can have various value types:
using var activity = activitySource.StartRichActivity("Operation", new
{
string_tag = "value", // string
int_tag = 42, // int
double_tag = 3.14, // double
bool_tag = true, // bool
enum_tag = HttpStatusCode.OK, // enum
guid_tag = Guid.NewGuid() // Guid
});
// All types are preserved in Tag structure
// Metric exporters handle type conversion as neededType handling by exporters: - Prometheus: Converts to strings - Application Insights: Preserves types as custom dimensions - OTLP: Supports typed attributes per OpenTelemetry spec
Performance Considerations
Configuration Freezing
Immutability for thread safety:
((IOptionsBasedMetricRecordingEnricherOptions)options.Freeze())- Options are frozen to immutable collections before use
- Prevents modification during concurrent access
- Creates immutable list copy only once per instrument
LINQ Pipeline Efficiency
Deduplication strategy:
instrumentTags.Concat(generalTags).Distinct()- Combines instrument-specific and general tag names
Distinct()removes duplicate names efficiently (hash-based)- Minimal allocations with deferred execution
Hierarchical search:
activity.GetAncestors(includeSelf: true)
.Select(a => a.GetTagItem(tagName))
.FirstOrDefault(v => v != null)FirstOrDefaultwith predicate enables early exit- Stops at first non-null value
- No unnecessary parent traversal
Memory Efficiency
Tag collection: - Returns IEnumerable<Tag> with deferred execution - Only materializes tags that exist in hierarchy - No allocations for missing tags
Best practices:
// ? Good - minimal tag list
"MetricTags": ["environment", "region", "tier"]
// ? Bad - excessive tags
"MetricTags": [ /* 20+ tag names */ ]Recommended Tag Counts
| Scenario | Tag Count | Rationale |
|---|---|---|
| Minimal | 2-3 tags | Environment, region, tier |
| Standard | 4-6 tags | + operation category, version |
| Detailed | 7-10 tags | + feature flags, deployment slot |
| Avoid | 10+ tags | Excessive cardinality and lookup overhead |
Thread Safety
The OptionsBasedMetricRecordingEnricher is thread-safe:
- ?
IOptionsMonitor<T>is thread-safe by design - ?
Freeze()creates immutable snapshot for evaluation - ? No mutable state between enrichment operations
- ? LINQ operations are stateless
- ? Activity tag lookup is thread-safe
Multiple threads can enrich different activities concurrently without synchronization issues.
Troubleshooting
Hierarchical Search Not Working
Symptoms: Parent tags not inherited by child activities.
Debugging:
public Tags ExtractTags(Activity activity, Instrument instrument)
{
// Debug: print hierarchy
foreach (var ancestor in activity.GetAncestors(includeSelf: true))
{
Console.WriteLine($"Activity: {ancestor.OperationName}");
foreach (var tag in ancestor.Tags)
{
Console.WriteLine($" {tag.Key} = {tag.Value}");
}
}
return base.ExtractTags(activity, instrument);
}Common issue: Activity hierarchy broken
// ? Wrong - creates unrelated activity
using var activity1 = activitySource.StartActivity("Parent");
using var activity2 = activitySource.StartActivity("Child");
// activity2.Parent != activity1 (both are children of current Activity)
// ? Correct - proper hierarchy
using var activity1 = activitySource.StartActivity("Parent");
// activity1 is now Activity.Current
using var activity2 = activitySource.StartActivity("Child");
// activity2.Parent == activity1 ?High Cardinality Issues
Symptoms: Excessive storage costs, slow queries.
Cause: Too many unique tag combinations.
Diagnosis:
// Check configured tags
var options = serviceProvider.GetService<IOptions<OptionsBasedMetricRecordingEnricherOptions>>();
Console.WriteLine($"Configured tags: {string.Join(", ", options.Value.MetricTags)}");
// Estimate cardinality
// If customer_id is configured: ? Millions of unique values
// If customer_tier is configured: ? ~3 unique valuesSolution: Review configured tags for cardinality:
{
"MetricTags": [
"customer_id" // ? Remove - high cardinality
"customer_tier", // ? Keep - low cardinality (3 values)
"region", // ? Keep - low cardinality (~10 values)
"request_path" // ? Remove - high cardinality (thousands)
"endpoint_category" // ? Keep - low cardinality (5 values)
]
}Performance Impact
Symptoms: Increased latency or CPU usage.
Diagnosis:
// Check enricher overhead
var stopwatch = Stopwatch.StartNew();
var tags = enricher.ExtractTags(activity, instrument).ToArray();
stopwatch.Stop();
Console.WriteLine($"Enrichment took: {stopwatch.ElapsedMilliseconds}ms");
Console.WriteLine($"Extracted {tags.Length} tags");Mitigation: - Reduce configured tag count (< 10 tags) - Limit activity hierarchy depth (< 10 levels) - Profile hierarchical search for bottlenecks - Consider caching for repeated extractions
Design Patterns
Strategy Pattern
The enricher implements the Strategy pattern: - IMetricRecordingEnricher defines the enrichment strategy interface - OptionsBasedMetricRecordingEnricher is one concrete strategy - SpanDurationMetricRecorder uses enrichers without knowing implementation details
Options Pattern
Uses the .NET Options pattern: - IOptionsMonitor<T> for reactive configuration - Named options for instrument-specific configuration - Hot reload support without restart
Chain of Responsibility
Multiple enrichers can be registered:
// Each enricher contributes tags
builder.Services.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, DeploymentEnricher>();
builder.Services.AddSingleton<IMetricRecordingEnricher, BusinessEnricher>();
// SpanDurationMetricRecorder collects tags from all enrichers
var allTags = enrichers.SelectMany(e => e.ExtractTags(activity, instrument));Template Method Pattern
Virtual ExtractTags method enables customization:
public class CustomEnricher : OptionsBasedMetricRecordingEnricher
{
public override Tags ExtractTags(Activity activity, Instrument instrument)
{
// Custom pre-processing
var baseTags = base.ExtractTags(activity, instrument).ToList();
// Add additional tags
baseTags.Add(new Tag("custom", "value"));
return baseTags;
}
}Best Practices
Tag Selection
? DO choose low-cardinality business dimensions:
{
"MetricTags": [
"customer_tier", // 3-5 values
"region", // ~10 values
"operation_category", // 5-10 values
"deployment_ring", // 3-4 values
"feature_flag_variant" // 2-3 values per flag
]
}? DON’T include high-cardinality identifiers:
{
"MetricTags": [
"customer_id", // ? Millions of values
"order_id", // ? Unlimited values
"request_id", // ? Unlimited values
"user_agent", // ? Thousands of values
"url_path" // ? Thousands of values
]
}Configuration Organization
? DO group by metric purpose:
{
"OptionsBasedMetricRecordingEnricher": {
"MetricTags": [
"environment",
"region",
"deployment_slot"
]
},
"diginsight.span_duration": {
"MetricTags": [
"operation_category",
"performance_tier"
]
},
"http.server.request.duration": {
"MetricTags": [
"endpoint_category",
"api_version"
]
}
}? DO use consistent tag naming:
{
"MetricTags": [
"customer_tier", // ? snake_case, descriptive
"deployment_region", // ? fully qualified
"api_version" // ? clear meaning
]
}? DON’T use inconsistent naming:
{
"MetricTags": [
"customerTier", // ? Mixed case
"region", // ? Ambiguous (deployment? customer?)
"v" // ? Unclear abbreviation
]
}Activity Tagging
? DO set tags during activity creation:
using var activity = activitySource.StartRichActivity("ProcessOrder", new
{
customer_tier = GetCustomerTier(),
region = GetRegion(),
operation_category = "order_processing"
});? DO set business context at root activity:
// HTTP middleware or entry point
using var rootActivity = activitySource.StartRichActivity("HandleRequest", new
{
customer_tier = context.GetCustomerTier(),
region = context.GetRegion(),
tenant_id = context.GetTenantId()
});
// Child activities inherit these automatically? DON’T duplicate tags on every child:
// ? Wasteful - region already set on parent
using var childActivity = activitySource.StartRichActivity("SubOperation", new
{
region = GetRegion() // Unnecessary duplication
});Testing Enrichers
[Fact]
public void ExtractTags_RetrievesFromHierarchy()
{
// Arrange
var options = new OptionsBasedMetricRecordingEnricherOptions
{
MetricTags = { "customer_tier", "region" }
};
var monitor = Mock.Of<IOptionsMonitor<OptionsBasedMetricRecordingEnricherOptions>>(
m => m.CurrentValue == options &&
m.Get(It.IsAny<string>()) == new OptionsBasedMetricRecordingEnricherOptions());
var enricher = new OptionsBasedMetricRecordingEnricher(monitor);
var parentSource = new ActivitySource("Parent");
var childSource = new ActivitySource("Child");
using var parent = parentSource.StartActivity("ParentOp")!;
parent.SetTag("customer_tier", "premium");
parent.SetTag("region", "us-east");
using var child = childSource.StartActivity("ChildOp")!;
// child doesn't have tags
var instrument = Mock.Of<Instrument>(i => i.Name == "test.metric");
// Act
var tags = enricher.ExtractTags(child, instrument).ToArray();
// Assert
Assert.Contains(tags, t => t.Key == "customer_tier" && t.Value.Equals("premium"));
Assert.Contains(tags, t => t.Key == "region" && t.Value.Equals("us-east"));
}Version History
| Version | Changes |
|---|---|
| 3.0.0 | Initial release with IMetricRecordingEnricher support |
| 3.1.0 | Added instrument-specific named configuration support |
| 3.2.0 | Improved hierarchical tag resolution performance |
See Also
- How Metric Recording Works with Diginsight and OpenTelemetry
- IMetricRecordingEnricher Interface
- OptionsBasedMetricRecordingEnricherOptions Class
- SpanDurationMetricRecorder Class
- OptionsBasedMetricRecordingFilter Class
- ActivityExtensions Class
- OpenTelemetry Semantic Conventions
Remarks
The OptionsBasedMetricRecordingEnricher provides a declarative approach to metric enrichment that promotes low-cardinality tagging through configuration. By searching the activity hierarchy, it enables setting business context once at the root activity level and automatically propagating it to all child operations’ metrics.
Design principles: - ?? Configuration over code - define tags in settings, not scattered through code - ?? Low cardinality by design - configuration makes high-cardinality tags obvious - ?? Hierarchical context - set once at root, inherit in children - ? Performance-optimized - early exit search with frozen options - ?? Hot reload - configuration changes apply immediately - ?? Composable - works alongside custom enrichers
Common use cases: - Add deployment context (environment, region, version) to all metrics - Include business dimensions (customer tier, tenant) for filtering - Tag with feature flags for A/B test analysis - Add operation categories for grouping and alerting - Include performance tiers for SLA monitoring
Integration:
builder.Services
.AddSingleton<IMetricRecordingEnricher, OptionsBasedMetricRecordingEnricher>()
.AddSpanDurationMetricRecorder();This configuration-based approach ensures that metric dimensions remain consistent, maintainable, and observable through version control, making it easier to reason about telemetry costs and cardinality in production environments.